package za.org.jcicada.genss.sensor.impl;

import java.util.Observable;

import org.apache.commons.math.stat.regression.SimpleRegression;

import za.org.jcicada.genss.sensor.ISensorValue;
import za.org.jcicada.genss.sensor.SensorURN;

/**
 * Sampling strategy that fits a line to a specified number of samples. New
 * samples are only added if they deviate by more than a specified threshold
 * from the predicted line or the status has changed.
 * @author rcrida
 */
public abstract class StrategyFit extends StrategyBase {
    
	private static final long serialVersionUID = 1L;
	double[] xHistory;
    double[] yHistory;
    int numHistory;
    double threshold;
    boolean validHistory;
    int nextIndex;
    double slope;
    double intercept;

    /**
     * Construct the strategy
     * @param sensorURN the sensor URI
     * @param numHistory the number of history points to use to fit the line
     * @param threshold maximum deviation from line before new sample is added
     */
    public StrategyFit(SensorURN sensorURN, int numHistory, double threshold) {
        super(sensorURN);
        this.numHistory = numHistory;
        xHistory = new double[numHistory];
        yHistory = new double[numHistory];
        validHistory = false;
        nextIndex = 0;
    }
    
    /**
     * Add a sample to the history. If the history length is long enough then
     * a flag is set to declare it valid for predictions and the line parameters
     * are estimated.
     */
    private void addSample(ISensorValue value) {
        long x = value.getTime();
        double y = getValueAsDouble(value);
        xHistory[nextIndex] = x;
        yHistory[nextIndex++] = y;
        if (nextIndex >= numHistory) {
            nextIndex = 0;
            validHistory = true;
        }
        if (validHistory) {
            SimpleRegression sr = new SimpleRegression();
            for (int i=0; i < numHistory; i++) {
                sr.addData(xHistory[i], yHistory[i]);
            }
            slope = sr.getSlope();
            intercept = sr.getIntercept();
        }
    }
    
    /**
     * Calculate the predicted value given the timestamp and estimated line
     * parameters.
     * @param x the timestamp
     * @return the predicted value
     */
    private double predict(double x) {
        return intercept + slope * x;
    }
    
    /**
     * Implement this in a derived class.
     * @return the value of the sensor as a double
     */
    protected abstract double getValueAsDouble(ISensorValue value);
    
    /**
     * If there is not yet sufficient history then a new sample is added. If
     * there is sufficient history then the new value is compared to the predicted
     * value and it is reported if it deviates by more than the specified threshold
     * or the status has changed.
     */
    @Override
    protected void updateValue(Observable o, ISensorValue value) {
        if (!validHistory) {
            addSample(value);
            notifySample(value);
        }
        else {
            if (Math.abs(predict(value.getTime()) - getValueAsDouble(value)) > threshold) {
                addSample(value);
                notifySample(value);
            }
        }
    }
    
}
